package com.jfinal.plugin.radius.util;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.util.Arrays;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import com.jfinal.log.Logger;
import com.jfinal.plugin.radius.attribute.RadiusAttribute;
import com.jfinal.plugin.radius.packet.AccessRequest;
import com.jfinal.plugin.radius.packet.AccountingRequest;
import com.jfinal.plugin.radius.packet.RadiusPacket;

/**
 * 实现一个简单的radius服务器。这个类必须被定义为getsharedsecret()和getuserpassword()实现。
 * 如果服务器支持审计，必须重载accountingrequestreceived()。
 */
public abstract class RadiusServer {
    
	private static final Logger logger = Logger.getLogger("RadiusPlugin");
    private final String mSingleSecret;
	
    /** */
    protected RadiusServer() {
        this(null);
    }
    
    /**
     * @param singleSecret use a single secret for all clients.
     */
    protected RadiusServer(String singleSecret) {
        super();
        this.mSingleSecret=singleSecret;
    }
    
	/**
	 * Returns the shared secret used to communicate with the client with the
	 * passed IP address or null if the client is not allowed at this server.
	 * @param client IP address and port number of client
	 * @return shared secret or null
	 */
	public abstract String getSharedSecret(InetSocketAddress client);
	
	/**
	 * Returns the password of the passed user. Either this
	 * method or accessRequestReceived() should be overriden.
	 * @param userName user name
	 * @return plain-text password or null if user unknown
	 */
	public abstract String getUserPassword(String userName);
	
	/**
	 * Constructs an answer for an Access-Request packet. Either this
	 * method or isUserAuthenticated should be overriden.
	 * @param accessRequest Radius request packet
	 * @param client address of Radius client
	 * @return response packet or null if no packet shall be sent
	 * @exception RadiusException malformed request packet; if this
	 * exception is thrown, no answer will be sent
	 */
	public RadiusPacket accessRequestReceived(AccessRequest accessRequest, InetSocketAddress client)
	throws RadiusException {
		String plaintext = getUserPassword(accessRequest.getUserName());
		int type = RadiusPacket.ACCESS_REJECT;
		if (plaintext != null && accessRequest.verifyPassword(plaintext))
		{
			type = RadiusPacket.ACCESS_ACCEPT;
		}
		
		RadiusPacket answer = new RadiusPacket(type, accessRequest.getPacketIdentifier());
		copyProxyState(accessRequest, answer);
		return answer;
	}
	
	/**
	 * Constructs an answer for an Accounting-Request packet. This method
	 * should be overriden if accounting is supported.
	 * @param accountingRequest Radius request packet
	 * @param client address of Radius client
	 * @return response packet or null if no packet shall be sent
	 * @exception RadiusException malformed request packet; if this
	 * exception is thrown, no answer will be sent
	 */
	public RadiusPacket accountingRequestReceived(AccountingRequest accountingRequest, InetSocketAddress client) 
	throws RadiusException {
		RadiusPacket answer = new RadiusPacket(RadiusPacket.ACCOUNTING_RESPONSE, accountingRequest.getPacketIdentifier());
		copyProxyState(accountingRequest, answer);
		return answer;
	}
	
	/**
	 * Starts the Radius server.
	 * @param listenAuth open auth port?
	 * @param listenAcct open acct port?
	 */
	public void start(boolean listenAuth, boolean listenAcct) {
		if (listenAuth) {
		    @SuppressWarnings("hiding")
            final Logger LOGGER = RadiusServer.logger;
			new Thread() {
				public void run() {
					setName("Radius Auth Listener");
					try {
						LOGGER.info("starting RadiusAuthListener on port " + getAuthPort());
						listenAuth();
						LOGGER.info("RadiusAuthListener is being terminated");
					} catch(Exception e) {
						LOGGER.fatal("auth thread stopped by exception", e);
					} finally {
						LOGGER.debug("auth socket closed");
					}
				}
			}.start();
		}
		
		if (listenAcct) {
            @SuppressWarnings("hiding")
            final Logger LOGGER = RadiusServer.logger;
			new Thread() {
				public void run() {
					setName("Radius Acct Listener");
					try {
						LOGGER.info("starting RadiusAcctListener on port " + getAcctPort());
						listenAcct();
						LOGGER.info("RadiusAcctListener is being terminated");
					} catch(Exception e) {
						LOGGER.fatal("acct thread stopped by exception", e);
					} finally {
						acctSocket.close();
						LOGGER.debug("acct socket closed");
					}
				}
			}.start();
		}
	}
	
    /**
     * Starts the Radius server.
     * @param listenAuth open auth port?
     * @param listenAcct open acct port?
	 * @throws SocketException socket exception.
     */
    public void start(int listenAuth, int listenAcct) throws SocketException {
        for(int i=0; i<listenAuth;i++) {
            @SuppressWarnings("hiding")
            final Logger LOGGER = RadiusServer.logger;
            new Thread() {
                private final DatagramSocket socket = getAuthSocket();
                public void run() {
                    setName("Radius Auth Listener");
                    try {
                        LOGGER.info("starting RadiusAuthListener on port " + getAuthPort());
                        listen(this.socket);
                        LOGGER.info("RadiusAuthListener is being terminated");
                    } catch(Exception e) {
                        LOGGER.fatal("auth thread stopped by exception", e);
                    } finally {
                        this.socket.close();
                        LOGGER.debug("auth socket closed");
                    }
                }
            }.start();
        }
        
        for(int i=0; i<listenAcct; i++) {
            @SuppressWarnings("hiding")
            final Logger LOGGER = RadiusServer.logger;
            new Thread() {
                private final DatagramSocket socket = getAcctSocket();
                public void run() {
                    setName("Radius Acct Listener");
                    try {
                        LOGGER.info("starting RadiusAcctListener on port " + getAcctPort());
                        listen(this.socket);
                        LOGGER.info("RadiusAcctListener is being terminated");
                    } catch(Exception e) {
                        LOGGER.fatal("acct thread stopped by exception", e);
                    } finally {
                        this.socket.close();
                        LOGGER.debug("acct socket closed");
                    }
                }
            }.start();
        }
    }
	
	/**
	 * Stops the server and closes the sockets.
	 */
	public void stop() {
		logger.info("stopping Radius server");
		closing = true;
		if (authSocket != null){
			authSocket.close();
		}
			
		if (acctSocket != null){
			acctSocket.close();
		}
			
	}
	
	/**
	 * Returns the auth port the server will listen on.
	 * @return auth port
	 */
	public int getAuthPort() {
		return authPort;
	}
	
	/**
	 * Sets the auth port the server will listen on.
	 * @param authPort auth port, 1-65535
	 */
	public void setAuthPort(int authPort) {
		if (authPort < 1 || authPort > 65535){
			throw new IllegalArgumentException("bad port number");
		}
			
		this.authPort = authPort;
		this.authSocket = null;
	}
	
	/**
	 * Returns the socket timeout (ms).
	 * @return socket timeout
	 */
	public int getSocketTimeout() {
		return socketTimeout;
	}
	
	/**
	 * Sets the socket timeout.
	 * @param socketTimeout socket timeout, >0 ms
	 * @throws SocketException
	 */
	public void setSocketTimeout(int socketTimeout)
	throws SocketException {
		if (socketTimeout < 1){
			throw new IllegalArgumentException("socket tiemout must be positive");
		}
			
		this.socketTimeout = socketTimeout;
		if (authSocket != null){
			authSocket.setSoTimeout(socketTimeout);
		}
			
		if (acctSocket != null){
			acctSocket.setSoTimeout(socketTimeout);
		}
			
	}
	
	/**
	 * Sets the acct port the server will listen on.
	 * @param acctPort acct port 1-65535
	 */
	public void setAcctPort(int acctPort) {
		if (acctPort < 1 || acctPort > 65535){
			throw new IllegalArgumentException("bad port number");
		}
			
		this.acctPort = acctPort;
		this.acctSocket = null;
	}

	/**
	 * Returns the acct port the server will listen on.
	 * @return acct port
	 */
	public int getAcctPort() {
		return acctPort;
	}
	
	/**
	 * Returns the duplicate interval in ms.
	 * A packet is discarded as a duplicate if in the duplicate interval
	 * there was another packet with the same identifier originating from the
	 * same address.
	 * @return duplicate interval (ms)
	 */
	public long getDuplicateInterval() {
		return duplicateInterval;
	}

	/**
	 * Sets the duplicate interval in ms.
	 * A packet is discarded as a duplicate if in the duplicate interval
	 * there was another packet with the same identifier originating from the
	 * same address.
	 * Setting to 0 disables the duplicate verification.
	 * @param duplicateInterval duplicate interval (ms), >=0
	 */
	public void setDuplicateInterval(long duplicateInterval) {
		if (duplicateInterval < 0){
			throw new IllegalArgumentException("duplicate interval must be positive");
		}
			
		this.duplicateInterval = duplicateInterval;
	}
	
	/**
	 * Returns the IP address the server listens on.
	 * Returns null if listening on the wildcard address.
	 * @return listen address or null
	 */
	public InetAddress getListenAddress() {
		return listenAddress;
	}
	
	/**
	 * Sets the address the server listens on.
	 * Must be called before start().
	 * Defaults to null, meaning listen on every
	 * local address (wildcard address).
	 * @param listenAddress listen address or null
	 */
	public void setListenAddress(InetAddress listenAddress) {
		this.listenAddress = listenAddress;
	}
	
	/**
	 * Copies all Proxy-State attributes from the request
	 * packet to the response packet.
	 * @param request request packet
	 * @param answer response packet
	 */
	protected void copyProxyState(RadiusPacket request, RadiusPacket answer) {
		List proxyStateAttrs = request.getAttributes(33);
		for (Iterator i = proxyStateAttrs.iterator(); i.hasNext();) {
			RadiusAttribute proxyStateAttr = (RadiusAttribute)i.next();
			answer.addAttribute(proxyStateAttr);
		}		
	}
	
	/**
	 * Listens on the auth port (blocks the current thread).
	 * Returns when stop() is called.
	 * @throws SocketException
	 * @throws InterruptedException
	 * 
	 */
	protected void listenAuth()
	throws SocketException {
		listen(getAuthSocket());
	}
		
	/**
	 * Listens on the acct port (blocks the current thread).
	 * Returns when stop() is called.
	 * @throws SocketException
	 * @throws InterruptedException
	 */
	protected void listenAcct()
	throws SocketException {
		listen(getAcctSocket());
	}

	/**
	 * Listens on the passed socket, blocks until stop() is called.
	 * @param s socket to listen on
	 */
	protected void listen(DatagramSocket s) {
		DatagramPacket packetIn = new DatagramPacket(new byte[RadiusPacket.MAX_PACKET_LENGTH], RadiusPacket.MAX_PACKET_LENGTH);
	    for(;;) {
			try {
			    for(;;) {
    				// receive packet
			        /*
    				try {
    				/* */
					s.receive(packetIn);
					if (logger.isDebugEnabled())
						logger.debug("receive buffer size = " + s.getReceiveBufferSize());
					/*
    				} catch (SocketException se) {
    					if (this.closing) {
    						// end thread
    						LOGGER.info("got closing signal - end listen thread");
    						return;
    					}
    					// retry s.receive()
                        if(LOGGER.isErrorEnabled())
    					    LOGGER.error("SocketException during s.receive() -> retry", se);
    					continue;
    				}
    				/* */
    								
    				// check client
    				InetSocketAddress localAddress = (InetSocketAddress)s.getLocalSocketAddress();
    				InetSocketAddress remoteAddress = new InetSocketAddress(packetIn.getAddress(), packetIn.getPort());				

    				/* FIXME DAVI IGNORE SECRET
    				String secret = this.mSingleSecret == null ? getSharedSecret(remoteAddress) : this.mSingleSecret;
    				if (secret == null) {
    					if (LOGGER.isInfoEnabled())
    						LOGGER.info("ignoring packet from unknown client " + remoteAddress + " received on local address " + localAddress);
    					continue;
    				}
    				*/
    				String secret = null; // DAVI
    				
    				// parse packet
    				RadiusPacket request = makeRadiusPacket(packetIn, secret);
    				if (logger.isDebugEnabled()){
    					logger.debug("received packet from " + remoteAddress + " on local address " + localAddress + ": " + request);
    				}
    					
    
    				RadiusPacket response = handlePacket(localAddress, remoteAddress, request, secret);
    				
    				/* FIXME DAVI */
    				response = null;
    				
    				// send response
    				if (response != null) {
    					if (logger.isDebugEnabled()){
    						logger.debug("send response: " + response);
    					}
    						
    					DatagramPacket packetOut = makeDatagramPacket(response, secret, remoteAddress.getAddress(), packetIn.getPort(), request);
    					s.send(packetOut);
    				} else{
    					if (logger.isInfoEnabled())
                        	logger.info("no response sent");
    				}
                        
			    }
            } catch (SocketException se) {
                if (this.closing) {
                    // end thread
                	logger.info("got closing signal - end listen thread");
                    return;
                }
                if(logger.isErrorEnabled()){
                	logger.error("SocketException during s.receive() -> retry", se);
                }
                	
			} catch (SocketTimeoutException ste) {
				// this is expected behaviour
                if(logger.isErrorEnabled()){
                	logger.error("normal socket timeout");
                }
                	
			} catch (IOException ioe) {
				// error while reading/writing socket
	             if(logger.isErrorEnabled()){
	            	 logger.error("communication error", ioe);
	             }
	            	 
			} catch (RadiusException re) {
				// malformed packet
                if(logger.isErrorEnabled()){
                	logger.error("malformed Radius packet", re);
                }
                	
            } catch (RuntimeException re) {
                // malformed packet
                if(logger.isErrorEnabled()){
                	logger.error("malformed Radius packet", re);
                }
                	
			}
		}
	}

	/**
	 * Handles the received Radius packet and constructs a response. 
	 * @param localAddress local address the packet was received on
	 * @param remoteAddress remote address the packet was sent by
	 * @param request the packet
	 * @return response packet or null for no response
	 * @throws RadiusException
	 */
	protected RadiusPacket handlePacket(InetSocketAddress localAddress, InetSocketAddress remoteAddress, RadiusPacket request, String sharedSecret) 
	throws RadiusException, IOException {
		RadiusPacket response = null;
		
		// check for duplicates
		if (this.duplicateInterval == 0 || !isPacketDuplicate(request, remoteAddress)) {
			if (localAddress.getPort() == getAuthPort()) {
				// handle packets on auth port
				if (request instanceof AccessRequest){
					response = accessRequestReceived((AccessRequest)request, remoteAddress);	
				}
				else{
					logger.error("unknown Radius packet type: " + request.getPacketType());
				}
			} else if (localAddress.getPort() == getAcctPort()) {
				// handle packets on acct port
				if (request instanceof AccountingRequest){
					response = accountingRequestReceived((AccountingRequest)request, remoteAddress);
				}
				else{
					logger.error("unknown Radius packet type: " + request.getPacketType());
				}
			} else {
				// ignore packet on unknown port
			}
		} else{
			logger.info("ignore duplicate packet");
		}
			

		return response;
	}

	/**
	 * Returns a socket bound to the auth port.
	 * @return socket
	 * @throws SocketException
	 */
	protected DatagramSocket getAuthSocket() 
	throws SocketException {
		if (authSocket == null) {
			if (getListenAddress() == null){
				authSocket = new DatagramSocket(getAuthPort());
			}
			else{
				authSocket = new DatagramSocket(getAuthPort(), getListenAddress());
			}
				
			authSocket.setSoTimeout(getSocketTimeout());
		}
		return authSocket;
	}

	/**
	 * Returns a socket bound to the acct port.
	 * @return socket
	 * @throws SocketException
	 */
	protected DatagramSocket getAcctSocket() 
	throws SocketException {
		if (acctSocket == null) {
			if (getListenAddress() == null){
				acctSocket = new DatagramSocket(getAcctPort());
			}
			else{
				acctSocket = new DatagramSocket(getAcctPort(), getListenAddress());
			}
			acctSocket.setSoTimeout(getSocketTimeout());
		}
		return acctSocket;
	}

	/**
	 * Creates a Radius response datagram packet from a RadiusPacket to be send. 
	 * @param packet RadiusPacket
	 * @param secret shared secret to encode packet
	 * @param address where to send the packet
	 * @param port destination port
	 * @param request request packet
	 * @return new datagram packet
	 * @throws IOException
	 */
	protected DatagramPacket makeDatagramPacket(RadiusPacket packet, String secret, InetAddress address, int port,
			RadiusPacket request) 
	throws IOException {
		ByteArrayOutputStream bos = new ByteArrayOutputStream();
		packet.encodeResponsePacket(bos, secret, request);
		byte[] data = bos.toByteArray();
	
		DatagramPacket datagram = new DatagramPacket(data, data.length, address, port);
		return datagram;
	}
	
	/**
	 * Creates a RadiusPacket for a Radius request from a received
	 * datagram packet.
	 * @param packet received datagram
	 * @return RadiusPacket object
	 * @exception RadiusException malformed packet
	 * @exception IOException communication error (after getRetryCount()
	 * retries)
	 */
	protected RadiusPacket makeRadiusPacket(DatagramPacket packet, String sharedSecret) 
	throws IOException, RadiusException {
		ByteArrayInputStream in = new ByteArrayInputStream(packet.getData());
		RadiusPacket r = RadiusPacket.decodeRequestPacket(in, sharedSecret);
		r.packetSize = packet.getLength();
		return r;
	}
	
	/**
	 * Checks whether the passed packet is a duplicate.
	 * A packet is duplicate if another packet with the same identifier
	 * has been sent from the same host in the last time. 
	 * @param packet packet in question
	 * @param address client address
	 * @return true if it is duplicate
	 */
	protected boolean isPacketDuplicate(RadiusPacket packet, InetSocketAddress address) {
		long now = System.currentTimeMillis();
		long intervalStart = now - getDuplicateInterval();
		
		byte[] authenticator = packet.getAuthenticator();
		
		synchronized(receivedPackets) {
			for (Iterator i = receivedPackets.iterator(); i.hasNext();) {
				ReceivedPacket p = (ReceivedPacket)i.next();
				if (p.receiveTime < intervalStart) {
					// packet is older than duplicate interval
					i.remove();
				} else {
					if (p.address.equals(address) && p.packetIdentifier == packet.getPacketIdentifier()) {
						if (authenticator != null && p.authenticator != null) {
							// packet is duplicate if stored authenticator is equal
							// to the packet authenticator
							return Arrays.equals(p.authenticator, authenticator);
						}
						// should not happen, packet is duplicate
						return true;
					}
				}
			}
		
			// add packet to receive list
			ReceivedPacket rp = new ReceivedPacket();
			rp.address = address;
			rp.packetIdentifier = packet.getPacketIdentifier();
			rp.receiveTime = now;
			rp.authenticator = authenticator;
			receivedPackets.add(rp);
		}

		return false;
	}

	private InetAddress listenAddress = null;
	private int authPort = 1812;
	private int acctPort = 1813;
	private DatagramSocket authSocket = null;
	private DatagramSocket acctSocket = null;
	private int socketTimeout = 3000;
	private List receivedPackets = new LinkedList();
	private long duplicateInterval = 30000; // 30 s
	private boolean closing = false;
	
}

/**
 * This internal class represents a packet that has been received by 
 * the server.
 */
final class ReceivedPacket {
	
	/**
	 * The identifier of the packet.
	 */
	public int packetIdentifier;
	
	/**
	 * The time the packet was received.
	 */
	public long receiveTime;
	
	/**
	 * The address of the host who sent the packet.
	 */
	public InetSocketAddress address;
	
	/**
	 * Authenticator of the received packet.
	 */
	public byte[] authenticator;
	
}
